// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: qiang

#include "coin2/exchange/bitget_futures/order_v1/parser.h"

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include <rapidjson/document.h>

#include "coin/proto/coin_query.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/bitget_futures/order_v1/error_code.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::bitget_futures::order_v1 {

namespace {

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::CurrencyTransferBundle;
using coin::proto::CurrencyTransferElement;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductFillElement;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;

using OrderContext = base::order::IOrderContext;
using coin2::exchange::bitget_futures::order_v1::error_code::GetExchangeOrderErrorCode;
using coin2::exchange::bitget_futures::order_v1::error_code::GetExchangeOrderErrorMsg;
using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
using OrderEventType = coin::proto::OrderEvent::OrderEventType;

inline int64_t GetProcOrderId(const std::string& proc_str) {
  int64_t proc_order_id = 0;
  try {
    proc_order_id = stoll(proc_str);
  } catch (std::exception& e) {
    LOG(ERROR) << e.what() << proc_str;
  }
  return proc_order_id;
}

inline coin::proto::OrderSide GetOrderSide(const std::string& side) {
  if (side == "open_long") {
    return coin::proto::OrderSide::BUY_OPEN_ORDER;
  } else if (side == "open_short") {
    return coin::proto::OrderSide::SELL_OPEN_ORDER;
  } else if (side == "close_long") {
    return coin::proto::OrderSide::SELL_CLOSE_ORDER;
  } else if (side == "close_short") {
    return coin::proto::OrderSide::BUY_CLOSE_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline coin::proto::OrderType GetOrderType(const std::string& type) {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

inline void SetOrderStatus(std::string_view status, ProductOrderElement* order) {
  if (status == "new" || status == "partially_filled" || status == "partial-fill") {
    order->set_is_live(true);
  } else if (status == "filled" || status == "full-fill") {
    order->set_fully_filled(true);
  } else if (status == "canceled" || status == "cancelled") {
    order->set_cancel_confirmed(true);
  } else {
    LOG(ERROR) << "[ParseRestOrderInfo] Unknown order status! " << status;
  }
}

inline coin::proto::OrderErrorCode GetOrderErrorCodeFromBitgetFuture(const JsonValue& doc) {
  using coin::proto::OrderErrorCode;
  auto result = coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR;
  std::string msg;
  int64_t code = std::numeric_limits<int64_t>::max();
  if (doc.HasMember("code")) {
    code = stoll(doc["code"].GetString());
  }
  if (doc.HasMember("msg")) {
    msg = doc["msg"].GetString();
  }

  if (code == 0 || msg == "success") {
    result = OrderErrorCode::ORDER_NO_ERROR;
  } else if (code == 40762 || msg == "The order size is greater than the max open size") {
    result = OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_FUND;
  } else if (code == 45111 || msg == "less than the minimum order quantity") {
    result = OrderErrorCode::SUBMIT_ORDER_WRONG_QTY;
  } else if (code == 40757 || msg == "Not enough position is available.") {
    result = OrderErrorCode::SUBMIT_ORDER_INSUFFICIENT_POSITION;
  } else if (code == 45110) {
    result = OrderErrorCode::SUBMIT_ORDER_WRONG_PRICE;
  } else if (code == 45115) {
    result = OrderErrorCode::SUBMIT_ORDER_WRONG_QTY;
  } else if (code == 40017) {
    result = OrderErrorCode::ORDER_CANCELED_ALREADY;
  } else {
    LOG(ERROR) << "Unknown error code: " << code;
  }

  return result;
}
}  // namespace

bool IsBitgetFuturesErrMessage(const JsonValue& doc) {
  if (!doc.HasMember("code") || !doc.HasMember("msg")) {
    return true;
  }

  auto code = stoll(doc["code"].GetString());
  if (code != 0 || std::string_view(doc["msg"].GetString()) != "success") {
    return true;
  }

  return false;
}

bool BitgetFuturesParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  if (!doc.HasMember("data")) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return false;
  }

  if (doc["data"].IsNull()) {
    result_.set_type(MsgType::IGNORABLE);
    return false;
  }

  for (const auto& data : doc["data"].GetArray()) {
    double avail = stod(data["crossMaxAvailable"].GetString());
    double total = stod(data["equity"].GetString());
    double hold = total - avail;
    CurrencyBalance balance;
    balance.set_currency_native(data["marginCoin"].GetString());
    balance.set_available(avail);
    balance.set_hold(hold);
    balance.set_total(total);
    raw_balance_map->emplace(balance.currency_native(), balance);
  }

  return true;
}

void BitgetFuturesParser::InitBalanceFromCacheMap(AccountBalance& acc_balance) {
  std::set<std::string> recorder;
  for (auto& [_, raw_balance_map] : balance_cache_map_) {
    for (auto& [native_currency, balance] : raw_balance_map) {
      if (symcache_->has_currency_native(native_currency)) {
        auto* currency_balance = acc_balance.add_each_balance();
        currency_balance->MergeFrom(balance);
        auto* currency = symcache_->GetCurrencyByNative(native_currency);
        currency_balance->set_currency(currency->currency());
        currency_balance->set_currency_native(currency->native_currency());
        recorder.emplace(native_currency);
      }
    }
  }

  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    if (recorder.count(native_currency) == 0) {
      auto* balance = acc_balance.add_each_balance();
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
      auto* currency = symcache_->GetCurrencyByNative(native_currency);
      balance->set_currency(currency->currency());
      balance->set_currency_native(currency->native_currency());
    }
  }
}

void BitgetFuturesParser::InitPositionFromCacheMap(AccountPosition& acc_position) {
  std::set<std::string> recorder;
  for (auto& [_, raw_position_map] : position_cache_map_) {
    for (auto& [native_symbol, pos] : raw_position_map) {
      auto* position = acc_position.add_each_position();
      position->MergeFrom(pos);
      position->set_native_symbol(native_symbol);
      recorder.emplace(native_symbol);
    }
  }

  for (const auto& native_symbol : symcache_->GetProductNative()) {
    if (recorder.count(native_symbol) == 0) {
      auto* position = acc_position.add_each_position();
      position->set_long_position(0);
      position->set_short_position(0);
      position->set_net_position(0);
      position->set_available_long_position(0);
      position->set_available_short_position(0);
      position->set_native_symbol(native_symbol);
    }
  }
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance BitgetFuturesParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  AccountBalance acc_balance;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  if (!is_account_balance_initialized_) {
    balance_cache_map_[msg_->http_context->extra_info] = std::move(raw_balance_map);
    if (balance_cache_map_.size() == GetProductTypes().size()) {
      InitBalanceFromCacheMap(acc_balance);
      acc_balance.set_exchange("Bitget");
      acc_balance.set_market_type("Futures");
      DLOG(INFO) << "Account BALANCE ready: " << acc_balance.DebugString() << std::endl;
      is_account_balance_initialized_ = true;
    }
    return acc_balance;
  }

  acc_balance.set_exchange("Bitget");
  acc_balance.set_market_type("Futures");
  for (auto& [native_currency, balance] : raw_balance_map) {
    if (symcache_->has_currency_native(native_currency)) {
      auto* balance = acc_balance.add_each_balance();
      balance->MergeFrom(raw_balance_map.at(native_currency));
      auto* currency = symcache_->GetCurrencyByNative(native_currency);
      balance->set_currency(currency->currency());
      balance->set_currency_native(currency->native_currency());
      DLOG(INFO) << "balance update: " << balance->DebugString() << std::endl;
    }
  }

  return acc_balance;
}

/*
{
  "code": "00000",
  "msg": "success",
  "requestTime": 0,
  "data": [
    {
      "marginCoin": "USDT",
      "locked": "0",
      "available": "1000",
      "crossMaxAvailable": "1000",
      "fixedMaxAvailable": "1000",
      "maxTransferOut": "1000",
      "equity": "1000",
      "usdtEquity": "1000",
      "btcEquity": "0.048740470425",
      "unrealizedPL": null
    }
  ]
}
*/
void BitgetFuturesParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!document.IsObject() || IsBitgetFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] " << JsonToString(document);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!is_account_balance_initialized_) {
    result_.set_type(MsgType::IGNORABLE);
    return;
  }

  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

bool BitgetFuturesParser::ParseRawPosition(const JsonValue& doc, RawPositionMap* raw_position_map) {
  /*
  {
    "code": "00000",
    "msg": "success",
    "requestTime": 0,
    "data": [
      {
        "marginCoin": "USDT",
        "symbol": "BTCUSDT_UMCBL",
        "holdSide": "long",
        "openDelegateCount": "0.001",
        "margin": "9.087675000001",
        "available": "0.01",
        "locked": "0",
        "total": "0.01",
        "leverage": 20,
        "achievedProfits": "0",
        "averageOpenPrice": "18175.350000000003",
        "marginMode": "crossed",
        "holdMode": "double_hold",
        "unrealizedPL": "-0.901900000001",
        "liquidationPrice": "0",
        "keepMarginRate": "0.004",
        "marketPrice": "18085.16",
        "cTime": "1667756835309"
      },
      {
        "marginCoin": "USDT",
        "symbol": "BTCUSDT_UMCBL",
        "holdSide": "short",
        "openDelegateCount": "0",
        "margin": "14.514475000001",
        "available": "0.016",
        "locked": "0",
        "total": "0.016",
        "leverage": 20,
        "achievedProfits": "0",
        "averageOpenPrice": "18143.093750000002",
        "marginMode": "crossed",
        "holdMode": "double_hold",
        "unrealizedPL": "0.92694",
        "liquidationPrice": "0",
        "keepMarginRate": "0.004",
        "marketPrice": "18085.16",
        "cTime": "1667756835309"
      }
    ]
  }
  */
  if (!doc.HasMember("data")) {
    LOG(ERROR) << "[ParseRawPosition] " << JsonToString(doc);
    return false;
  }

  if (doc["data"].IsNull()) {
    result_.set_type(MsgType::IGNORABLE);
    return false;
  }

  for (const auto& data : doc["data"].GetArray()) {
    if (!data.HasMember("symbol") || !data.HasMember("total") || !data.HasMember("holdSide")) {
      LOG(ERROR) << "Unexpected position data! " << JsonToString(data);
      continue;
    }
    auto native_symbol = data["symbol"].GetString();
    auto& position = (*raw_position_map)[native_symbol];
    double pos_amt = stod(data["total"].GetString());
    double pos_available = stod(data["available"].GetString());
    if (std::string_view(data["holdMode"].GetString()) == "double_hold") {
      if (std::string_view(data["holdSide"].GetString()) == "long") {  // long or short
        position.set_long_position(pos_amt);
        position.set_available_long_position(pos_available);
      } else {
        position.set_short_position(pos_amt);
        position.set_available_short_position(pos_available);
      }
      position.set_net_position(position.long_position() - position.short_position());
    } else {
      NOTREACHED() << "Not implemented Bitget holdMode != double_hold";
      position.set_net_position(pos_amt);
    }
  }

  return true;
}

void BitgetFuturesParser::ParseRestAllOrders() {
  is_query_historical_orders_ = true;
  ParseRestOrderInfoList();
}

AccountPosition BitgetFuturesParser::ParsePosition(const JsonValue& doc) {
  RawPositionMap raw_position_map;
  AccountPosition acc_position;
  auto success = ParseRawPosition(doc, &raw_position_map);
  if (!success) {
    return {};
  }

  if (!is_account_position_initialized_) {
    position_cache_map_[msg_->http_context->extra_info] = std::move(raw_position_map);
    if (position_cache_map_.size() == GetProductTypes().size()) {
      InitPositionFromCacheMap(acc_position);
      acc_position.set_exchange("Bitget");
      acc_position.set_market_type("Futures");
      is_account_position_initialized_ = true;
      DLOG(INFO) << "Account position ready: " << acc_position.DebugString() << std::endl;
    }
    return acc_position;
  }

  acc_position.set_exchange("Bitget");
  acc_position.set_market_type("Futures");
  for (auto& [native_symbol, pos] : raw_position_map) {
    auto* position = acc_position.add_each_position();
    *position = std::move(pos);
    position->set_native_symbol(native_symbol);
  }

  return acc_position;
}

void BitgetFuturesParser::ParseRestAccountPosition() {
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseRestAccountPosition] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!doc.IsObject() || IsBitgetFuturesErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return;
  }

  auto account_position = ParsePosition(doc);
  if (!is_account_position_initialized_) {
    result_.set_type(MsgType::IGNORABLE);
    return;
  }

  if (!account_position.has_exchange() || !account_position.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  result_.set_type(MsgType::ACCOUNT_POSITION);
  *result_.mutable_account_position() = std::move(account_position);
}

ProductOrderElement BitgetFuturesParser::ParseRestOrderInfo(const JsonValue& data) {
  ProductOrderElement order;
  std::string native_symbol = data["symbol"].GetString();
  if (!symcache_->has_product_native(native_symbol)) {
    return order;
  }
  int64_t order_timestamp_ft = msg_->timestamp;
  order.set_market_type(MarketType::Futures);
  order.set_exchange_type(ExchangeType::Bitget);
  order.set_api_version("v1");
  order.set_exchange_order_id(data["orderId"].GetString());
  order.set_symbol(native_symbol);
  order.set_price(stod(data["price"].GetString()));
  order.set_qty(stod(data["size"].GetString()));
  order.set_proc_order_id(GetProcOrderId(data["clientOid"].GetString()));
  order.set_side(GetOrderSide(data["side"].GetString()));
  order.set_order_type(GetOrderType(data["orderType"].GetString()));
  order.set_timestamp(order_timestamp_ft);
  SetOrderStatus(data["state"].GetString(), &order);

  return order;
}

void BitgetFuturesParser::ParseRestOrderInfoList() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (!document.IsObject() || IsBitgetFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(document);
    return;
  }

  JsonValue* p_data = nullptr;
  if (is_query_historical_orders_ && document.HasMember("data") &&
      document["data"].HasMember("orderList")) {
    p_data = &document["data"]["orderList"];
  } else if (!is_query_historical_orders_ && document.HasMember("data")) {
    p_data = &document["data"];
  }
  if (!p_data) {
    LOG(ERROR) << "[ParseRestOrderInfoList] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  const JsonValue& data = *p_data;
  if (data.IsNull()) {
    return;
  }

  if (!data.IsArray()) {
    LOG(ERROR) << "Unexpected order list format!" << JsonToString(data);
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  auto mutable_fill_list = result_.mutable_fill_list();
  mutable_fill_list->Clear();
  bool product_order_bundle_found = false;
  for (const auto& info : data.GetArray()) {
    auto new_order = ParseRestOrderInfo(info);
    auto* order = product_order_bundle->add_each_order();
    order->MergeFrom(new_order);
    product_order_bundle_found = true;
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

ProductFillElement BitgetFuturesParser::ParseRestFillInfo(const JsonValue& data) {
  ProductFillElement fill;
  fill.set_symbol(data["symbol"].GetString());
  fill.set_fill_timestamp(msg_->timestamp);
  fill.set_exchange_order_id(data["orderId"].GetString());
  fill.set_proc_order_id(GetProcOrderId(data["clientOid"].GetString()));
  fill.set_price(stod(data["price"].GetString()));
  fill.set_qty(stod(data["size"].GetString()));
  fill.set_fill_type(FillType::UNKNOWN_FILL_TYPE);

  return fill;
}

void BitgetFuturesParser::ParseSubmitResponse() {
  /*
  {
      "code":"00000",
      "data":{
          "orderId":"1627293504612",
          "clientOid":"BITGET#1627293504612"
      },
      "msg":"success",
      "requestTime":1627293504612
  }
  */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.base().result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[BitgetFuturesParser] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[BitgetFuturesParser] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[BitgetFuturesParser] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (!document.IsObject() || IsBitgetFuturesErrMessage(document)) {
    resp->set_error_code(GetOrderErrorCodeFromBitgetFuture(document));
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(document));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(document));
    resp->set_success(false);
    return;
  }

  if (!document.HasMember("data") || document["data"].IsNull() ||
      !document["data"].HasMember("orderId")) {
    LOG(ERROR) << "[BitgetFuturesParser Submit Order] exchange order id not found"
               << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  resp->set_exchange_order_id(document["data"]["orderId"].GetString());
}

void BitgetFuturesParser::ParseCancelResponse() {
  /*
{
  "code": "00000",
  "msg": "success",
  "requestTime": 1668341220426,
  "data": {
    "orderId": "975658738603696128",
    "clientOid": "dd543222a1aaa2122asaada21132qqqqqqaaq"
  }
}
  */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.base().result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[BitgetFuturesCancelOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[BitgetFuturesCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[BitgetFuturesCancelOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (!document.IsObject() || IsBitgetFuturesErrMessage(document)) {
    resp->set_error_code(GetOrderErrorCodeFromBitgetFuture(document));
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(document));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(document));
    resp->set_success(false);
    return;
  }

  if (!document.HasMember("data") || document["data"].IsNull() ||
      !document["data"].HasMember("orderId")) {
    LOG(ERROR) << "[ParseCancelResponse] Error Response" << http_context->res;
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  resp->set_cancel_confirmed(false);
}

void BitgetFuturesParser::ParseRestOpenOrders() {
  /*
  {
    "code": "00000",
    "msg": "success",
    "requestTime": 1668343911722,
    "data": [
      {
        "symbol": "BTCUSDT_UMCBL",
        "size": 0.001,
        "orderId": "975668579506233344",
        "clientOid": "dd1234",
        "filledQty": 0.000,
        "fee": 0E-8,
        "price": 400000.00,
        "state": "new",
        "side": "close_long",
        "timeInForce": "normal",
        "totalProfits": 0E-8,
        "posSide": "long",
        "marginCoin": "USDT",
        "filledAmount": 0.0000,
        "orderType": "limit",
        "leverage": "20",
        "marginMode": "crossed",
        "reduceOnly": true,
        "cTime": "1668343437077",
        "uTime": "1668343437077"
      }
    ]
  }
  */
  is_query_historical_orders_ = false;
  ParseRestOrderInfoList();
}

bool IsAccountUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  return json.HasMember("arg") && json["arg"].IsObject() && json["arg"].HasMember("channel") &&
         std::string_view(json["arg"]["channel"].GetString()) == "account";
}

bool BitgetFuturesParser::ParseRawBalanceFromWs(
    const JsonValue& doc,
    RawBalanceMap* raw_balance_map) {
  /*
  {
    "action": "snapshot",
    "arg": {
      "instType": "umcbl",
      "channel": "account",
      "instId": "default"
    },
    "data": [
      {
        "marginCoin": "USDT",
        "locked": "0.00000000",
        "available": "1000.73457869",
        "maxOpenPosAvailable": "1000.73457869",
        "maxTransferOut": "1000.73457869",
        "equity": "1000.73457869",
        "usdtEquity": "1000.734578694995"
      }
    ]
  }
  */
  if (!doc.IsObject()) {
    LOG(ERROR) << "[ParseRawBalanceFromWs] " << JsonToString(doc);
    return false;
  }
  if (!doc.HasMember("data") || !doc["data"].IsArray()) {
    LOG(ERROR) << "[ParseRawBalanceFromWs] " << JsonToString(doc);
    return false;
  }

  for (const auto& currency_bal : doc["data"].GetArray()) {
    if (!currency_bal.HasMember("equity") || !currency_bal.HasMember("maxOpenPosAvailable")) {
      LOG(ERROR) << "[ParseRawBalanceFromWs] " << JsonToString(doc);
      continue;
    }
    auto free = stod(currency_bal["maxOpenPosAvailable"].GetString());
    auto total = stod(currency_bal["equity"].GetString());
    CurrencyBalance balance;
    balance.set_currency_native(currency_bal["marginCoin"].GetString());
    balance.set_available(free);
    balance.set_hold(total - free);
    balance.set_total(total);
    raw_balance_map->emplace(balance.currency_native(), balance);
  }

  return true;
}

bool BitgetFuturesParser::ParseWsBalance(const JsonValue& doc, AccountBalance* acc_balance) {
  acc_balance->Clear();

  RawBalanceMap raw_balance_map;
  bool success = ParseRawBalanceFromWs(doc, &raw_balance_map);
  if (!success) {
    return false;
  }

  // update account balance, just publish delta change from ws update
  acc_balance->set_exchange("Bitget");
  acc_balance->set_market_type("Futures");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    if (raw_balance_map.count(native_currency) == 0) {
      continue;
    }
    auto* balance = acc_balance->add_each_balance();
    balance->MergeFrom(raw_balance_map.at(native_currency));
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
  }

  return true;
}

inline coin::proto::OrderSide GetWsOrderSide(const std::string& pos_side, const std::string& side) {
  if (pos_side == "long" && side == "buy") {
    return coin::proto::OrderSide::BUY_OPEN_ORDER;
  } else if (pos_side == "short" && side == "sell") {
    return coin::proto::OrderSide::SELL_OPEN_ORDER;
  } else if (pos_side == "long" && side == "sell") {
    return coin::proto::OrderSide::SELL_CLOSE_ORDER;
  } else if (pos_side == "short" && side == "buy") {
    return coin::proto::OrderSide::BUY_CLOSE_ORDER;
  } else {
    NOTREACHED() << "Unknown OrderSide";
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

ProductOrderBundle BitgetFuturesParser::ParseWsOrderInfoListV1(const JsonValue& data) {
  /*
  {
    "action": "snapshot",
    "arg": {
        "instType": "umcbl",
        "channel": "orders",
        "instId": "default"
    },
    "data": [
        {
            "accFillSz": "0",
            "cTime": "1667987897495",
            "clOrdId": "1667987872547188017",
            "force": "post_only",
            "instId": "BTCUSDT_UMCBL",
            "lever": "20",
            "notionalUsd": "17.6215",
            "ordId": "974177338415292418",
            "ordType": "limit",
            "orderFee": [
                {
                    "feeCcy": "USDT",
                    "fee": "0"
                }
            ],
            "posSide": "long",
            "px": "17621.5",
            "side": "buy",
            "status": "cancelled",
            "sz": "0.001",
            "tdMode": "cross",
            "tgtCcy": "USDT",
            "uTime": "1667987898734"
        }
    ]
}
  */
  if (!data.HasMember("data") || !data["data"].IsArray()) {
    return {};
  }

  ProductOrderBundle product_order_bundle;
  for (const auto& data : data["data"].GetArray()) {
    auto* order = product_order_bundle.add_each_order();
    order->set_market_type(MarketType::Futures);
    order->set_exchange_type(ExchangeType::Bitget);
    order->set_api_version("v1");
    order->set_exchange_order_id(data["ordId"].GetString());
    order->set_proc_order_id(GetProcOrderId(data["clOrdId"].GetString()));
    order->set_symbol(data["instId"].GetString());
    order->set_price(stod(data["px"].GetString()));
    order->set_qty(stod(data["sz"].GetString()));
    order->set_side(GetWsOrderSide(data["posSide"].GetString(), data["side"].GetString()));
    order->set_order_type(GetOrderType(data["ordType"].GetString()));
    SetOrderStatus(data["status"].GetString(), order);
    order->set_timestamp(GetCurrentTimestamp());
  }

  return product_order_bundle;
}

bool IsPositionUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  return json.HasMember("arg") && json["arg"].IsObject() && json["arg"].HasMember("channel") &&
         std::string_view(json["arg"]["channel"].GetString()) == "positions";
}

bool IsOrderUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  return json.HasMember("arg") && json["arg"].IsObject() && json["arg"].HasMember("channel") &&
         std::string_view(json["arg"]["channel"].GetString()) == "orders";
}

ProductFillBundle BitgetFuturesParser::ParseWsFillInfoListV1(const JsonValue& data) {
  /*
  {
        "action": "snapshot",
        "arg": {
          "instType": "umcbl",
          "channel": "orders",
          "instId": "default"
        },
        "data": [
          {
            "accFillSz": "0.001",
            "avgPx": "16752.5",
            "cTime": 1668480719744,
            "clOrdId": "1668480465398186487",
            "execType": "M",
            "fillFee": "-0.0033505",
            "fillFeeCcy": "USDT",
            "fillNotionalUsd": "16.7525",
            "fillPx": "16752.5",
            "fillSz": "0.001",
            "fillTime": "1668480721432",
            "force": "post_only",
            "instId": "BTCUSDT_UMCBL",
            "lever": "20",
            "low": false,
            "notionalUsd": "16.7525",
            "ordId": "976244384745562119",
            "ordType": "limit",
            "orderFee": [
              {
                "feeCcy": "USDT",
                "fee": "-0.0033505"
              }
            ],
            "pnl": "0",
            "posSide": "long",
            "px": "16752.5",
            "side": "buy",
            "status": "full-fill",
            "sz": "0.001",
            "tdMode": "cross",
            "tgtCcy": "USDT",
            "tradeId": "976244391829757954",
            "uTime": 1668480721432
          }
  */
  if (!data.HasMember("data") || !data["data"].IsArray()) {
    return {};
  }
  ProductFillBundle product_fill_bundle;
  for (const auto& data : data["data"].GetArray()) {
    std::string status;
    if (data.HasMember("status")) {
      status = data["status"].GetString();
    }

    if (status == "partial-fill" || status == "full-fill") {
      auto* fill = product_fill_bundle.add_each_fill();
      fill->set_symbol(data["instId"].GetString());
      fill->set_fill_timestamp(msg_->timestamp);
      fill->set_exchange_order_id(data["ordId"].GetString());
      fill->set_proc_order_id(GetProcOrderId(data["clOrdId"].GetString()));
      fill->set_price(stod(data["fillPx"].GetString()));
      fill->set_qty(stod(data["fillSz"].GetString()));
      fill->set_fill_id((data["tradeId"].GetString()));
      if (data.HasMember("orderFee") && data["orderFee"].IsArray()) {
        for (const auto& fee_data : data["orderFee"].GetArray()) {
          if (fee_data.HasMember("feeCcy") && fee_data.HasMember("fee")) {
            fill->set_fee_currency(fee_data["feeCcy"].GetString());
            fill->set_fee(std::abs(stod(fee_data["fee"].GetString())));
          }
        }
      }
      std::string fill_type = data["execType"].GetString();
      if (fill_type == "T") {
        fill->set_fill_type(FillType::TAKER_FILL_TYPE);
      } else if (fill_type == "M") {
        fill->set_fill_type(FillType::MAKER_FILL_TYPE);
      } else {
        fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
      }
    }
  }

  return product_fill_bundle;
}

bool BitgetFuturesParser::ParseWsPosition(const JsonValue& doc, AccountPosition* acc_position) {
  /*
  {
    "action": "snapshot",
    "arg": {
      "instType": "umcbl",
      "channel": "positions",
      "instId": "default"
    },
    "data": [
      {
        "posId": "973208193368498177",
        "instId": "BTCUSDT_UMCBL",
        "instName": "BTCUSDT",
        "marginCoin": "USDT",
        "margin": "4.5601",
        "marginMode": "crossed",
        "holdSide": "long",
        "holdMode": "double_hold",
        "total": "0.005",
        "available": "0.005",
        "locked": "0",
        "averageOpenPrice": "18240.6",
        "leverage": "20",
        "achievedProfits": "0",
        "upl": "-0.9594",
        "uplRate": "-0.2103",
        "liqPx": "-981949.14",
        "keepMarginRate": "0.004",
        "marginRate": "0.000414948967",
        "cTime": "1667756835309",
        "uTime": "1667980475121",
        "markPrice": "18048.71"
      },
      {
        "posId": "973208193368498179",
        "instId": "BTCUSDT_UMCBL",
        "instName": "BTCUSDT",
        "marginCoin": "USDT",
        "margin": "3.6432",
        "marginMode": "crossed",
        "holdSide": "short",
        "holdMode": "double_hold",
        "total": "0.004",
        "available": "0.004",
        "locked": "0",
        "averageOpenPrice": "18216.37",
        "leverage": "20",
        "achievedProfits": "0",
        "upl": "0.6706",
        "uplRate": "0.184",
        "liqPx": "-981949.14",
        "keepMarginRate": "0.004",
        "marginRate": "0.000414948967",
        "cTime": "1667756835309",
        "uTime": "1667980516589",
        "markPrice": "18048.71"
      }
    ]
  }
  */
  acc_position->Clear();
  if (!doc.IsObject()) {
    LOG(ERROR) << "[ParseWsPosition] Invalid json object";
    return false;
  }

  if (!doc.HasMember("data") || !doc["data"].IsArray()) {
    LOG(ERROR) << "[ParseWsPosition] Unexpected positions format" << JsonToString(doc);
    return false;
  }

  RawPositionMap raw_position_map;
  acc_position->set_exchange("Bitget");
  acc_position->set_market_type("Futures");
  const auto& position_list = doc["data"];
  for (const auto& data : position_list.GetArray()) {
    if (!data.HasMember("instId") || !data.HasMember("total") || !data.HasMember("holdMode")) {
      LOG(ERROR) << "[ParseWsPositionFromChanges] Unexpected position format" << JsonToString(data);
      continue;
    }

    auto native_symbol = data["instId"].GetString();
    double pos_amt = stod(data["total"].GetString());
    double pos_available = stod(data["available"].GetString());
    auto& position = raw_position_map[native_symbol];
    if (std::string_view(data["holdMode"].GetString()) == "double_hold") {
      if (std::string_view(data["holdSide"].GetString()) == "long") {  // long or short
        position.set_long_position(pos_amt);
        position.set_available_long_position(pos_available);
      } else {
        position.set_short_position(pos_amt);
        position.set_available_short_position(pos_available);
      }
      position.set_net_position(position.long_position() - position.short_position());
    } else {
      NOTREACHED() << "Not implemented Bitget holdMode != double_hold";
      position.set_net_position(pos_amt);
    }
  }

  for (auto& [native_symbol, pos] : raw_position_map) {
    auto* position = acc_position->add_each_position();
    *position = std::move(pos);
    position->set_native_symbol(native_symbol);
  }

  return true;
}

void BitgetFuturesParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.HasMember("event") && document.HasMember("code")) {
    if (std::string_view(document["event"].GetString()) == "error") {
      LOG(ERROR) << "[ParseWsMessage] Connection Error " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_NETWORK_ERROR);
      return;
    }
  }

  if (document.HasMember("event") &&
      std::string_view(document["event"].GetString()) == "subscribe") {
    LOG(ERROR) << "[ParseWsMessage] websocket connection successful " << JsonToString(document);
    result_.set_type(MsgType::IGNORABLE);
    return;
  }

  if (IsAccountUpdate(document)) {
    if (!is_account_balance_initialized_) {
      result_.set_type(MsgType::IGNORABLE);
      return;
    }
    if (!ParseWsBalance(document, result_.mutable_account_balance())) {
      result_.set_error_code(OrderErrorCode::INVALID_JSON);
      return;
    }
    result_.set_type(MsgType::ACCOUNT_BALANCE);
    last_ws_balance_timestamp_ = GetCurrentTimestamp();
  } else if (IsPositionUpdate(document)) {
    if (!is_account_position_initialized_) {
      result_.set_type(MsgType::IGNORABLE);
      return;
    }
    if (!ParseWsPosition(document, result_.mutable_account_position())) {
      result_.set_error_code(OrderErrorCode::INVALID_JSON);
      return;
    }
    result_.set_type(MsgType::ACCOUNT_POSITION);
    last_ws_balance_timestamp_ = GetCurrentTimestamp();
  } else if (IsOrderUpdate(document)) {
    *result_.mutable_fill_list() = ParseWsFillInfoListV1(document);
    *result_.mutable_order_list() = ParseWsOrderInfoListV1(document);
    result_.set_type(MsgType::ORDER_STATUS);
    result_.set_type(MsgType::ORDER_FILL);
  } else {
    LOG(ERROR) << "[ParseWsMessage] Unsupported message. " << msg_->DebugString();
  }
}
void BitgetFuturesParser::ParseRestFillInfoList() {
  /*
  {
      "code":"00000",
      "data":[
          {
              "tradeId":"802377534023585793",
              "symbol":"BTCUSDT_UMCBL",
              "orderId":"802377533381816325",
              "price":"0",
              "sizeQty":"0.3247",
              "fee":"0E-8",
              "side":"burst_close_long",
              "cTime":"1627027632241"
          }
      ],
      "msg":"success",
      "requestTime":1627386245672
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (!document.HasMember("data") || !document["data"].IsArray() ||
      IsBitgetFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(document);
    return;
  }

  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  for (const auto& data : document["data"].GetArray()) {
    auto* fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(data["symbol"].GetString());
    fill->set_fill_timestamp(msg_->timestamp);
    fill->set_exchange_order_id(data["orderId"].GetString());
    fill->set_price(stod(data["price"].GetString()));
    fill->set_qty(stod(data["sizeQty"].GetString()));
    fill->set_fill_id(data["tradeId"].GetString());
    fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
    product_fill_bundle_found = true;
  }
  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

auto BitgetFuturesParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case OrderSubTopicId::REST_ACCOUNT_POSITION:
      ParseRestAccountPosition();
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseRestOpenOrders();
      break;
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::REST_ALL_ORDERS:
      ParseRestAllOrders();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }
  return result_;
}
}  // namespace coin2::exchange::bitget_futures::order_v1
